Retrieved from https://overwatchleague.com/en-us/statslab.
The dataset provides the information of all matches in Overwatch League (OWL) from Seasons 2018 to 2021. OWL is a professional esports league for the game Overwatch which is a six-versus-six team based first-person shooter video game. Each match's information regarding match id, map types, map names, teams, players and played heros along with played hero stats was recorded in "phs*.csv", whereas the information about the results of eaeach match was recorded in "match_map_stats.csv".
import os
import dask
import dask.dataframe as dd
import pandas as pd
import numpy as np
import datetime as dt
from glob import glob
import plotly.graph_objects as go
import chart_studio.plotly as py
import cufflinks as cf
import plotly.express as px
# set plotly+cufflinks in offline mode
from plotly.offline import iplot, init_notebook_mode
init_notebook_mode(connected=True)
cf.go_offline(connected=True)
# SETTING THE THEME IN CUFFLINKS
cf.set_config_file(theme='pearl')
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
import jupyter_dash
from dask.distributed import Client
client=Client()
client
Client
|
Cluster
|
filenames=glob(os.path.join("Overwatch", "phs_*.csv"))
cols=['start_time', 'match_id', 'stage', 'map_type', 'map_name', 'player','team', 'stat_name', 'hero', 'stat_amount']
first = 'start_time,match_id,stage,map_type,map_name,player,team,stat_name,hero,stat_amount'
for file in filenames:
df=pd.read_csv(file)
if (df.columns!=cols).any():
print(file,"\n",df.columns)
sh.sed("-i", "1s/.*/" + first + "/", file)
%cd Dask Assignment
/home/phatdo/jeannie/Dask Assignment
dataset=dd.read_csv(os.path.join("Overwatch", "phs_*.csv"), parse_dates=["start_time"]).sort_values("start_time")
dataset.head()
| start_time | match_id | stage | map_type | map_name | player | team | stat_name | hero | stat_amount | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2018-01-11 00:12:00 | 10223 | Overwatch League - Stage 1 | PAYLOAD | Dorado | Agilities | Los Angeles Valiant | All Damage Done | All Heroes | 18079.206920 |
| 519 | 2018-01-11 00:12:00 | 10223 | Overwatch League - Stage 1 | PAYLOAD | Dorado | Nevix | San Francisco Shock | Shots Fired | All Heroes | 16081.000000 |
| 520 | 2018-01-11 00:12:00 | 10223 | Overwatch League - Stage 1 | PAYLOAD | Dorado | Nevix | San Francisco Shock | Solo Kills | All Heroes | 3.000000 |
| 521 | 2018-01-11 00:12:00 | 10223 | Overwatch League - Stage 1 | PAYLOAD | Dorado | Nevix | San Francisco Shock | Time Building Ultimate | All Heroes | 632.506018 |
| 522 | 2018-01-11 00:12:00 | 10223 | Overwatch League - Stage 1 | PAYLOAD | Dorado | Nevix | San Francisco Shock | Time Elapsed per Ultimate Earned | All Heroes | 126.501204 |
dataset.tail()
| start_time | match_id | stage | map_type | map_name | player | team | stat_name | hero | stat_amount | |
|---|---|---|---|---|---|---|---|---|---|---|
| 409107 | 2021-06-07 00:10:28 | 37401 | OWL 2021 | HYBRID | Hollywood | Masaa | Atlanta Reign | Quick Melee Accuracy | Lúcio | 0.166667 |
| 409108 | 2021-06-07 00:10:28 | 37401 | OWL 2021 | HYBRID | Hollywood | Masaa | Atlanta Reign | Quick Melee Hits | Lúcio | 1.000000 |
| 409109 | 2021-06-07 00:10:28 | 37401 | OWL 2021 | HYBRID | Hollywood | Masaa | Atlanta Reign | Quick Melee Ticks | Lúcio | 6.000000 |
| 409141 | 2021-06-07 00:10:28 | 37401 | OWL 2021 | HYBRID | Hollywood | Masaa | Atlanta Reign | Healing Received | Mercy | 1674.121271 |
| 409745 | 2021-06-07 00:10:28 | 37401 | OWL 2021 | HYBRID | Hollywood | super | San Francisco Shock | Weapon Accuracy | Orisa | 0.412257 |
print(f'There are total {len(dataset.index.compute())} rows and {len(dataset.columns)} columns in the dataset')
There are total 4325271 rows and 10 columns in the dataset
dataset.isnull().sum().compute()
start_time 0 match_id 0 stage 0 map_type 0 map_name 0 player 0 team 0 stat_name 0 hero 0 stat_amount 0 dtype: int64
dataset.groupby([dataset.start_time.dt.to_period("M"), dataset.stage])['match_id'].nunique().compute().reset_index()
| start_time | stage | match_id | |
|---|---|---|---|
| 0 | 2018-01 | Overwatch League - Stage 1 | 36 |
| 1 | 2018-02 | Overwatch League - Stage 1 | 24 |
| 2 | 2018-02 | Overwatch League - Stage 1 - Title Matches | 2 |
| 3 | 2018-02 | Overwatch League - Stage 2 | 12 |
| 4 | 2018-03 | Overwatch League - Stage 2 | 48 |
| 5 | 2018-03 | Overwatch League - Stage 2 Title Matches | 2 |
| 6 | 2018-04 | Overwatch League - Stage 3 | 48 |
| 7 | 2018-05 | Overwatch League - Stage 3 | 12 |
| 8 | 2018-05 | Overwatch League - Stage 3 Title Matches | 3 |
| 9 | 2018-05 | Overwatch League - Stage 4 | 28 |
| 10 | 2018-06 | Overwatch League - Stage 4 | 33 |
| 11 | 2018-06 | Overwatch League - Stage 4 Title Matches | 3 |
| 12 | 2018-07 | Overwatch League Inaugural Season Championship | 12 |
| 13 | 2019-02 | Overwatch League Stage 1 | 32 |
| 14 | 2019-03 | Overwatch League Stage 1 | 38 |
| 15 | 2019-03 | Overwatch League Stage 1 Title Matches | 7 |
| 16 | 2019-04 | Overwatch League Stage 2 | 56 |
| 17 | 2019-05 | Overwatch League Stage 2 | 14 |
| 18 | 2019-05 | Overwatch League Stage 2 Title Matches | 7 |
| 19 | 2019-06 | Overwatch League Stage 3 | 61 |
| 20 | 2019-07 | Overwatch League Stage 3 | 9 |
| 21 | 2019-07 | Overwatch League Stage 3 Title Matches | 7 |
| 22 | 2019-07 | Overwatch League Stage 4 | 16 |
| 23 | 2019-08 | Overwatch League Stage 4 | 54 |
| 24 | 2019-08 | Overwatch League 2019 Post-Season | 4 |
| 25 | 2019-09 | Overwatch League 2019 Post-Season | 15 |
| 26 | 2020-02 | OWL 2020 Regular Season | 21 |
| 27 | 2020-03 | OWL 2020 Regular Season | 21 |
| 28 | 2020-04 | OWL 2020 Regular Season | 39 |
| 29 | 2020-05 | OWL 2020 Regular Season | 52 |
| 30 | 2020-06 | OWL 2020 Regular Season | 35 |
| 31 | 2020-07 | OWL 2020 Regular Season | 45 |
| 32 | 2020-08 | OWL 2020 Regular Season | 52 |
| 33 | 2020-09 | OWL 2020 Regular Season | 26 |
| 34 | 2020-09 | OWL APAC All-Stars | 12 |
| 35 | 2020-10 | OWL 2020 Regular Season | 6 |
| 36 | 2020-10 | OWL North America All-Stars | 10 |
| 37 | 2021-04 | OWL 2021 | 34 |
| 38 | 2021-05 | OWL 2021 | 44 |
| 39 | 2021-06 | OWL 2021 | 20 |
dataset.groupby([dataset.start_time.dt.to_period("Y")])['team'].nunique().compute().reset_index()
| start_time | team | |
|---|---|---|
| 0 | 2018 | 12 |
| 1 | 2019 | 20 |
| 2 | 2020 | 49 |
| 3 | 2021 | 20 |
Why was the number of teams in 2020 over doubled that in the previous year? Bcecause it included the sub-teams or special one-person teams participating in OWL APAC All-Stars, and OWL North America All-Stars. This analysis will consider only regular OWL, so excluding the data relating the OWL All-Stars.
df=dataset[(dataset.stage!="OWL APAC All-Stars") & (dataset.stage!="OWL North America All-Stars") & (dataset.hero!="All Heroes")]
team_num=df.groupby([df.start_time.dt.to_period("Y")])['team'].nunique().compute().reset_index()
team_num
| start_time | team | |
|---|---|---|
| 0 | 2018 | 12 |
| 1 | 2019 | 20 |
| 2 | 2020 | 20 |
| 3 | 2021 | 20 |
This section will look into the popularity of heroes in OWL based on the indicator "Time Played" with multiple filters of season, hero type, and map type. There are three hero types (Tank, Damage, and Support), but the original dataset lacks the information about hero types. Hence, after extracting df_sub from the dataset with including the only stat "Time Played", the information about hero types is also filled in the new column "role" in df_sub.
df_sub=df[df.stat_name=="Time Played"]
tank=["D.Va", "Orisa", "Reinhardt", "Roadhog", "Sigma", "Winston", "Wrecking Ball", "Zarya"]
damage=["Ashe", "Bastion", "Doomfist", "Echo", "Genji", "Hanzo", "Junkrat", "McCree", "Mei", "Pharah", "Reaper", "Soldier: 76", "Sombra", "Symmetra", "Torbjörn", "Tracer", "Widowmaker"]
support=["Ana", "Baptiste", "Brigitte", "Lúcio", "Mercy","Moira", "Zenyatta"]
df_sub["role"]="Damage"
df_sub["role"]=df_sub["role"].where(~df_sub["hero"].isin(tank),"Tank")
df_sub["role"]=df_sub["role"].where(~df_sub["hero"].isin(support),"Support")
After that, the dataframe of "hero_timeplayed" will be retrieved from df_sub to get Total Played Time of each hero in each season, along with the information of hero types. Following that, the percentage of total time that a hero was played out of the Total Real Played Time of a season (calculated for both teams, i.e., double the actual real-life time) will be calculated.
hero_timeplayed=df_sub.groupby([df_sub.start_time.dt.to_period("Y"),df_sub.role, df_sub.hero]).stat_amount.sum().compute().reset_index()
hero_timeplayed["percentage"]=0
for i in hero_timeplayed.start_time.unique():
hero_timeplayed.percentage[hero_timeplayed.start_time==i]=hero_timeplayed[hero_timeplayed.start_time==i].stat_amount/(hero_timeplayed.groupby("start_time").stat_amount.sum()[i]/6)
Meanwhile, the dataframe "hero_maptime" will get Total Played Time of each hero in each map type in each season, along with the information of hero types. The percentage of total time that a hero was played in a map type out of the Total Real Played Time in the map type of a season (calculated for both two teams) will be calculated
hero_maptime=df_sub.groupby([df_sub.map_type, df_sub.role, df_sub.start_time.dt.to_period("Y"), df_sub.hero]).stat_amount.sum().compute().reset_index()
hero_maptime["percentage"]=0
for i in hero_maptime.start_time.unique():
for j in hero_maptime.map_type.unique():
hero_maptime.percentage[(hero_maptime.start_time==i)&(hero_maptime.map_type==j)]=hero_maptime[(hero_maptime.start_time==i)&(hero_maptime.map_type==j)].stat_amount/(hero_maptime.groupby(["start_time", "map_type"]).stat_amount.sum()[i,j]/6)
The dataframe "df_sub_compute" will be used to get the distribution of Played Time of each hero per game, in multiple contexts of season and map type.
df_sub_compute=df_sub.compute().reset_index(drop=True)
app = jupyter_dash.JupyterDash(__name__)
app.layout = html.Div([
html.Div([
dcc.Dropdown(
id="dropdown",
options=[{"label": str(x), "value": str(x)} for x in hero_timeplayed.start_time.unique()],
value=str(hero_timeplayed.start_time.unique()[0]),
clearable=False),
html.Label("Hero Type"),
dcc.RadioItems(
id="herotype",
options=[{"label": x, "value": x} for x in ["All", "Tank", "Damage", "Support"]],
value="All"),
html.Label("Map Type"),
dcc.RadioItems(
id="maptype",
options=[{"label": x, "value": x} for x in ["All", "ASSAULT", "CONTROL", "HYBRID", "PAYLOAD"]],
value="All")]),
html.Div([
dcc.Graph(id="bar-chart"),
dcc.Graph(id="box-plot")
])
])
@app.callback(
Output("bar-chart", "figure"),
Output("box-plot", "figure"),
Input("dropdown", "value"),
Input("herotype", "value"),
Input("maptype", "value"))
def figure_display(year, herotype, maptype):
graph=[]
if maptype=="All":
frame=hero_timeplayed[hero_timeplayed.start_time==year].reset_index(drop=True)
frame1=df_sub_compute[df_sub_compute.start_time.dt.to_period("Y")==year].reset_index(drop=True)
if herotype =="All":
fig=px.bar(frame, x="hero", y="percentage", color="hero", title="Hero Usage by Total Played Time")
fig.update_layout(yaxis=dict(tickformat=".0%"),xaxis={'categoryorder':'total descending'}, font_family="Arial")
fig1=px.box(frame1, x=frame1.hero, y=frame1.stat_amount/60, color="hero", title="Distribution of Played Time per Game")
fig1.update_layout(yaxis_title='minute', xaxis_title='hero', xaxis={'categoryorder':'total descending'})
graph.append(fig)
graph.append(fig1)
else:
fig=px.bar(frame[frame.role==herotype], x="hero", y="percentage", color="hero", title="Hero Usage by Total Played Time for "+herotype)
fig.update_layout(yaxis=dict(tickformat=".0%"),xaxis={'categoryorder':'total descending'}, font_family="Arial")
fig1=px.box(frame1[frame1.role==herotype], x="hero", y=frame1[frame1.role==herotype].stat_amount/60, color="hero", title="Distribution of Played Time per Game for "+herotype)
fig1.update_layout(yaxis_title='minute', xaxis_title='hero', xaxis={'categoryorder':'total descending'})
graph.append(fig)
graph.append(fig1)
else:
frame=hero_maptime[hero_maptime.start_time==year].reset_index(drop=True)
frame1=df_sub_compute[df_sub_compute.start_time.dt.to_period("Y")==year].reset_index(drop=True)
if herotype=="All":
fig=px.bar(frame[frame.map_type==maptype], x="hero", y="percentage", color="hero", title="Hero Usage by Total Played Time in "+maptype )
fig.update_layout(yaxis=dict(tickformat=".0%"),xaxis={'categoryorder':'total descending'}, font_family="Arial")
fig1=px.box(frame1[frame1.map_type==maptype], x="hero", y=frame1[frame1.map_type==maptype].stat_amount/60, color="hero", title="Distribution of Played Time per Game in "+maptype)
fig1.update_layout(yaxis_title='minute', xaxis_title='hero', xaxis={'categoryorder':'total descending'})
graph.append(fig)
graph.append(fig1)
else:
fig=px.bar(frame[(frame.map_type==maptype)&(frame.role==herotype)], x="hero", y="percentage", color="hero", title="Hero Usage by Total Played Time for "+herotype+" in "+maptype)
fig.update_layout(yaxis=dict(tickformat=".0%"),xaxis={'categoryorder':'total descending'}, font_family="Arial")
fig1=px.box(frame1[(frame1.map_type==maptype)&(frame1.role==herotype)], x="hero", y=frame1[(frame1.map_type==maptype)&(frame1.role==herotype)].stat_amount/60, color="hero", title="Distribution of Played Time per Game for "+herotype+" in "+maptype)
fig1.update_layout(yaxis_title='minute', xaxis_title='hero', xaxis={'categoryorder':'total descending'})
graph.append(fig)
graph.append(fig1)
return graph
if __name__ == '__main__':
app.run_server(mode="inline",debug=True)
/home/phatdo/miniconda3/lib/python3.9/site-packages/jupyter_dash/jupyter_app.py:139: UserWarning: The 'environ['werkzeug.server.shutdown']' function is deprecated and will be removed in Werkzeug 2.1.
In on-going OWL 2021:
- Those findings are in accordance with the uprising trend of Dive strategy in OWL. Usually, the Dive strategy involves a line up with two tanks D.Va and Winston and two supports from the possible combinations of the most four popular support heroes.
- That is probably because of the introduction of new hero Echo that makes the Double-Shield strategy less favourable. The Double-Shield strategy, in which teams were most likely to pick 2 shield tanks, was dominant in OWL 2020, and also the result of the role lock enforcement (picking 2 tanks-2 damages-2 supports) from the developers to eliminate the Goat strategy (in which teams were the most likely to pick 3 tanks and 3 supports) which was dominant in OWL 2019.
The Hero Usage Stats are one of the information sources for the developers to keep an eyes on the status of OWL, to find a clue on what they need to intervene, and to verify the effects of their adjusments
The previous dataset does not include the information of the result of each game in each match in OWL, thus it needs to be merge with the dataset from "match_map_stats.csv". The dataframe "hero_mapresult" is the result of the merging.
df1=df_sub.groupby([df_sub.start_time,"match_id", "map_type","map_name", "team", "role"]).hero.unique().compute().reset_index()
df_sub_compute.hero.unique()
array(['Tracer', 'D.Va', 'Genji', 'Junkrat', 'Mercy', 'Zenyatta',
'Winston', 'McCree', 'Widowmaker', 'Lúcio', 'Moira', 'Orisa',
'Sombra', 'Soldier: 76', 'Pharah', 'Ana', 'Bastion', 'Doomfist',
'Reaper', 'Zarya', 'Roadhog', 'Reinhardt', 'Hanzo', 'Mei',
'Torbjörn', 'Symmetra', 'Brigitte', 'Wrecking Ball', 'Ashe',
'Baptiste', 'Sigma', 'Echo'], dtype=object)
result=pd.read_csv("./Overwatch/match_map_stats.csv", parse_dates=["round_start_time"])
a=result.drop_duplicates(subset=["match_id", 'game_number'])
a=a[(a.stage!="OWL APAC All-Stars") & (a.stage!="OWL North America All-Stars")]
b=a[["match_id","game_number","map_name","map_winner","map_loser","round_start_time"]]
b.reset_index(inplace=True, drop=True)
b["result"]="victory"
hero_mapresult = pd.merge(df1, b, on = ['match_id', 'map_name'], how='left')
for i in hero_mapresult.index:
if hero_mapresult.loc[i,"team"]!=hero_mapresult.loc[i,"map_winner"]:
hero_mapresult.loc[i,"result"]="defeat"
if hero_mapresult.loc[i, "map_winner"]=="draw":
hero_mapresult.loc[i,"result"]="draw"
hero_mapresult
| start_time | match_id | map_type | map_name | team | role | hero | game_number | map_winner | map_loser | round_start_time | result | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2018-01-11 00:12:00 | 10223 | PAYLOAD | Dorado | Los Angeles Valiant | Damage | [Genji, Widowmaker, Tracer] | 1.0 | Los Angeles Valiant | San Francisco Shock | 2018-01-11 00:12:07 | victory |
| 1 | 2018-01-11 00:12:00 | 10223 | PAYLOAD | Dorado | Los Angeles Valiant | Support | [Zenyatta, Mercy] | 1.0 | Los Angeles Valiant | San Francisco Shock | 2018-01-11 00:12:07 | victory |
| 2 | 2018-01-11 00:12:00 | 10223 | PAYLOAD | Dorado | Los Angeles Valiant | Tank | [D.Va, Winston] | 1.0 | Los Angeles Valiant | San Francisco Shock | 2018-01-11 00:12:07 | victory |
| 3 | 2018-01-11 00:12:00 | 10223 | PAYLOAD | Dorado | San Francisco Shock | Damage | [Tracer, Genji, Junkrat, McCree] | 1.0 | Los Angeles Valiant | San Francisco Shock | 2018-01-11 00:12:07 | defeat |
| 4 | 2018-01-11 00:12:00 | 10223 | PAYLOAD | Dorado | San Francisco Shock | Support | [Mercy, Zenyatta] | 1.0 | Los Angeles Valiant | San Francisco Shock | 2018-01-11 00:12:07 | defeat |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 23624 | 2021-06-07 00:10:28 | 37401 | HYBRID | Hollywood | Atlanta Reign | Support | [Baptiste, Mercy, Lúcio] | 3.0 | Atlanta Reign | San Francisco Shock | 2021-06-07 00:10:26 | victory |
| 23625 | 2021-06-07 00:10:28 | 37401 | HYBRID | Hollywood | Atlanta Reign | Tank | [Wrecking Ball, Sigma, Orisa] | 3.0 | Atlanta Reign | San Francisco Shock | 2021-06-07 00:10:26 | victory |
| 23626 | 2021-06-07 00:10:28 | 37401 | HYBRID | Hollywood | San Francisco Shock | Damage | [Echo, Soldier: 76, Ashe] | 3.0 | Atlanta Reign | San Francisco Shock | 2021-06-07 00:10:26 | defeat |
| 23627 | 2021-06-07 00:10:28 | 37401 | HYBRID | Hollywood | San Francisco Shock | Support | [Lúcio, Mercy, Baptiste] | 3.0 | Atlanta Reign | San Francisco Shock | 2021-06-07 00:10:26 | defeat |
| 23628 | 2021-06-07 00:10:28 | 37401 | HYBRID | Hollywood | San Francisco Shock | Tank | [Sigma, Wrecking Ball, Orisa] | 3.0 | Atlanta Reign | San Francisco Shock | 2021-06-07 00:10:26 | defeat |
23629 rows × 12 columns
From the dataframe "hero_mapresult", the dataframe "hero_performance" will be formed to have the information of the win rate and the pick rate of each hero per season.
frame=[]
for i in df_sub_compute.hero.unique():
check=hero_mapresult.hero.apply(lambda x: i in x)
#print(check)
percentage=hero_mapresult[check==True].groupby([hero_mapresult.start_time.dt.to_period("Y"),"result"]).result.count()*100/hero_mapresult[check==True].groupby(hero_mapresult.start_time.dt.to_period("Y")).result.count()
table=percentage.to_frame().rename(columns={"result": "percent"}).reset_index()
table["hero"]=i
#print(table)
pick=(hero_mapresult[check==True].groupby([hero_mapresult.start_time.dt.to_period("Y")]).result.count()*100/hero_mapresult.groupby([hero_mapresult.start_time.dt.to_period("Y")]).result.count()).to_frame()
final=table.merge(pick, on="start_time", how="left")
frame.append(final)
#print(table)
hero_performance=pd.concat(frame)
hero_performance.reset_index(drop=True, inplace=True)
hero_performance.rename(columns={"result_y": "pick_rate"}, inplace=True)
hero_performance.rename(columns={"result_x": "result"}, inplace=True)
hero_performance
| start_time | result | percent | hero | pick_rate | |
|---|---|---|---|---|---|
| 0 | 2018 | defeat | 50.831703 | Tracer | 30.722982 |
| 1 | 2018 | draw | 2.348337 | Tracer | 30.722982 |
| 2 | 2018 | victory | 46.819961 | Tracer | 30.722982 |
| 3 | 2019 | defeat | 56.766917 | Tracer | 13.502538 |
| 4 | 2019 | draw | 3.947368 | Tracer | 13.502538 |
| ... | ... | ... | ... | ... | ... |
| 359 | 2020 | draw | 2.313625 | Echo | 5.682150 |
| 360 | 2020 | victory | 44.473008 | Echo | 5.682150 |
| 361 | 2021 | defeat | 49.667406 | Echo | 20.044444 |
| 362 | 2021 | draw | 3.769401 | Echo | 20.044444 |
| 363 | 2021 | victory | 46.563193 | Echo | 20.044444 |
364 rows × 5 columns
hero_performance[(hero_performance.result=="victory") & (hero_performance.start_time=="2018") & (hero_performance.hero=="Tracer")]['pick rate'].values[0]
0.3072298211333233
from PIL import Image
heroes_list = hero_performance.hero.unique()
for i in hero_performance.start_time.unique():
fig=px.scatter(hero_performance[(hero_performance.result=="victory") & (hero_performance.start_time==i)], x="pick_rate", y="percent", color="hero", title=str(i), labels=dict(pick_rate='pick rate (%)', percent='win rate (%)'))
for hero in heroes_list:
# print(hero)
if hero == 'Soldier: 76':
fig.add_layout_image(
dict(
source=Image.open("/home/phatdo/jeannie/Dask Assignment/Overwatch/heroes/new/new/" + "Soldier 76_portrait" + ".png"),
xref='x',
yref='y',
x=hero_performance[(hero_performance.result=="victory") & (hero_performance.start_time==i) & (hero_performance.hero==hero)]['pick_rate'].values[0],
y=hero_performance[(hero_performance.result=="victory") & (hero_performance.start_time==i) & (hero_performance.hero==hero)]['percent'].values[0],
sizex=4,
sizey=4,
# sizing="stretch",
# opacity=0.5,
layer="below")
)
else:
try:
fig.add_layout_image(
dict(
source=Image.open("/home/phatdo/jeannie/Dask Assignment/Overwatch/heroes/new/new/" + hero + "_portrait.png"),
xref='x',
yref='y',
x=hero_performance[(hero_performance.result=="victory") & (hero_performance.start_time==i) & (hero_performance.hero==hero)]['pick_rate'].values[0],
y=hero_performance[(hero_performance.result=="victory") & (hero_performance.start_time==i) & (hero_performance.hero==hero)]['percent'].values[0],
sizex=4,
sizey=4,
# sizing="stretch",
# opacity=0.5,
layer="below"))
except: continue
fig.update_xaxes(range=[0, 35])
fig.update_yaxes(range=[30, 60])
#fig.update_layout(yaxis=dict(tickformat="{:.f}%"), xaxis=dict(tickformat="{:.f}%"))
# xaxis = dict(tickmode = 'linear', tick0 = 0, dtick = 5),
# yaxis = dict(tickmode = 'linear', tick0 = 30, dtick = 5))
fig.show()
On the x-axis (pick rate), the heroes on the left are the least popular (picked the least) and the ones on the right are the most popular. Similarly, on the y-axis (win rate), the heroes at the top are the ones most likely to win and the ones at the bottom are the least likely to win.
The distance between the data points for all heroes are a general indicator of how diverse the strategies in the OWL are, or in other words, how 'balanced' the game is. In this aspect, 2020 seemed to be the most balanced (and thus diverse).
Some noticable cases: Bastion in 2021 and Symmetra in 2018 both have very high win rates but extremely low pick rates. This is because of their design characteristics, which make them only viable in very specific and limited situations, but are also very likely to lead to victories in such situations.